# -*- coding: UTF-8 -*-
#/**
# * Software Name : pycrate
# * Version : 0.3
# *
# * Copyright 2017. Benoit Michau. ANSSI.
# *
# * This library is free software; you can redistribute it and/or
# * modify it under the terms of the GNU Lesser General Public
# * License as published by the Free Software Foundation; either
# * version 2.1 of the License, or (at your option) any later version.
# *
# * This library is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# * Lesser General Public License for more details.
# *
# * You should have received a copy of the GNU Lesser General Public
# * License along with this library; if not, write to the Free Software
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
# * MA 02110-1301  USA
# *
# *--------------------------------------------------------
# * File Name : pycrate_asn1rt/asnobj.py
# * Created : 2017-01-31
# * Authors : Benoit Michau 
# *--------------------------------------------------------
#*/

from .utils   import *
from .err     import *
from .refobj  import *
from .dictobj import *
from .setobj  import *
from .codecs  import *


ASN1Obj_docstring = """
Common object attributes:
    
    - name: str, the identifier of the object
    
    - mode: str (MODE_*), the mode of the object after compilation.
        - MODE_TYPE : ASN.1 subtype or subclass
        - MODE_VALUE: ASN.1 value
        - MODE_SET  : ASN.1 set of values
    
    - param: bool, True in case the object is a parameterized one.
        Parameterized objects are "empty" and mainly useless for encoding / decoding.
        They are still required to keep track of object references and tag chain.
    
    - tag: None or tuple, contains the tagging of the ASN.1 type.
        If defined, it has the following format:
        (tag value (int),
         tag class (str in TAG_CONTEXT_SPEC / TAG_PRIVATE / TAG_APPLICATION / TAG_UNIVERSAL),
         tag mode (str in TAG_IMPLICIT / TAG_EXPLICIT))
    
    - typeref: None or ASN1Ref, provides the reference to the subtype of the object 
        in case it derives from another user-defined one (and not a native one).
    
    - tr: None or ASN1Obj, provides the actual subtype object
    
    - cont: type-dependent.
    
    - root: None or list of str, provides the identifiers of the root content if 
        defined.
    
    - ext: None or list of str, provides the identifiers of the extended content if
        defined.
    
    - val: None for MODE_TYPE,
           single_value (type-dependent) for MODE_VALUE,
           ASN1Set for MODE_SET.

Attributes for object generic constraints:
    
    - const_val: None or ASN1Set, provides the set of values that constraints the type
    
    - const_tab: None or ASN1Obj (MODE_SET), provides the CLASS set for table lookup
    
    - const_tab_id: None or str, only set if const_tab is set,
        provides the identifier that corresponds to self in const_tab
    
    - const_tab_at: None or tuple of str, only set if const_tab is set,
        provides the path of identifiers to which self must refer for the table lookup in const_tab

Attributes defined for components of a constructed or CLASS type:
    
    - parent: None or ASN1Obj, indicates the parent ASN.1 object container
    
    - opt: bool, indicates if the component is OPTIONAL
    
    - def: None or single value, indicates the DEFAULT value
    
    - uniq: bool, indicates if the component is UNIQUE
    
    - group: None or int, indicates the extension group index for grouped extended components

Special attributes that exists only for some objects:
    
    - root_mand (for SEQUENCE, SET, CLASS)
    - root_opt (for SEQUENCE, SET, CLASS)
    - cont_rev (for ENUM)
    - cont_tags (for CHOICE, SEQUENCE, SET)
    - ext_ident (for CHOICE, SEQUENCE, SET)
    - ext_group (for CHOICE, SEQUENCE, SET)
    - ext_group_obj (for SEQUENCE, SET)
    - ext_nest (for SEQUENCE, SET)
    - defby (for ANY)

Attributes for object specific constraints:
    
    - const_sz (for BIT STRING, OCTET STRING, *String, SEQUENCE OF, SET OF)
    - const_ind (for ENUM and CHOICE)
    - const_cont (for BIT STRING, OCTET STRING)
    - const_cont_enc (for BIT STRING, OCTET STRING)
    - const_alpha (for *String)

Attributes used at run-time:
    
    - struct: None or pycrate.elt.Element instance, provides the complete 
        structure of the transfer syntax with, generated when encoding and 
        decoding the object when using from_*_ws() and to_*_ws() methods.
"""

class ASN1Obj(Element):
    
    # in order to disable any asnlog() during the runtime
    _SILENT = False
    
    # this enables object verification during Python module initialization
    _SAFE_INIT   = True
    # this enables object value verification when using set_val()
    _SAFE_VAL    = True
    # this enables object's constraints verification when using set_val() or 
    # set_val_unsafe()
    _SAFE_BND    = True
    # this enables object's table constraint verification when using set_val() 
    # or set_val_unsafe()
    _SAFE_BNDTAB = True
    
    #--------------------------------------------------------------------------#
    # class attributes, initialization and safe checking methods
    #--------------------------------------------------------------------------#
    
    _name    = ''
    _mode    = MODE_TYPE
    _tag     = None
    _typeref = None
    _tr      = None
    _param   = False
    _parent  = None
    _opt     = False
    _def     = None
    _uniq    = False
    _group   = None
    _cont    = None
    _root    = None
    _ext     = None
    _val     = None
    
    _const_val    = None
    # _const_sz is only defined for types which can have a SIZE constraint
    #_const_sz     = None
    # _const_alpha is only defined for _String subtypes
    #_const_alpha  = None
    # _const_cont is only defined for BIT STRING and OCTET STRING
    #_const_cont   = None
    #_const_cont_enc = None
    _const_tab    = None
    # _const_tab_id and _const_tab_at are only defined if _const_tab is not None
    #_const_tab_id = None
    #_const_tab_at = None
    
    
    TYPE = None
    TAG  = None
    
    def __init__(self, **kwargs):
        if 'name'      in kwargs: self._name    = kwargs['name']
        if 'mode'      in kwargs: self._mode    = kwargs['mode']
        if 'tag'       in kwargs: self._tag     = kwargs['tag']
        if 'typeref'   in kwargs: self._typeref = kwargs['typeref']
        if 'param'     in kwargs: self._param   = kwargs['param']
        if 'opt'       in kwargs: self._opt     = kwargs['opt']
        elif 'default' in kwargs: self._def     = kwargs['default']
        if 'defby'     in kwargs: self._defby   = kwargs['defby']
        if 'uniq'      in kwargs: self._uniq    = kwargs['uniq']
        if 'group'     in kwargs: self._group   = kwargs['group']
    
    def _safechk_obj(self):
        """
        ensures all internal attributes at initialization have the correct format
        """
        # name
        if not isinstance(self._name, str_types):
            raise(ASN1ObjErr('invalid name, {0!r}'.format(self._name)))
        # mode
        if self._mode not in (MODE_TYPE, MODE_SET, MODE_VALUE):
            raise(ASN1ObjErr('{0}: invalid mode, {1!r}'\
                  .format(self.fullname(), self._mode)))
        # tag
        if not isinstance(self._tag, (NoneType, tuple)):
            raise(ASN1ObjErr('{0}: invalid tag, {1!r}'\
                  .format(self.fullname(), self._tag)))
        elif isinstance(self._tag, tuple) and ( \
        len(self._tag) != 3 or \
        not isinstance(self._tag[0], integer_types) or \
        self._tag[1] not in (TAG_CONTEXT_SPEC, TAG_PRIVATE, TAG_APPLICATION, TAG_UNIVERSAL) or \
        self._tag[2] not in (TAG_IMPLICIT, TAG_EXPLICIT)):
            raise(ASN1ObjErr('{0}: invalid tag, {1!r}'\
                  .format(self.fullname(), self._tag)))
        # typeref
        if not isinstance(self._typeref, (NoneType, ASN1Ref)):
            raise(ASN1ObjErr('{0}: invalid typeref, {1!r}'\
                  .format(self.fullname(), self._typeref)))
        # param
        if not isinstance(self._param, bool):
            raise(ASN1ObjErr('{0}: invalid param, {1!r}'\
                  .format(self.fullname(), self._param)))
        # parent
        if not isinstance(self._parent, (NoneType, ASN1Obj)):
            raise(ASN1ObjErr('{0}: invalid parent, {1!r}'\
                  .format(self.fullname(), self._parent)))
        # opt
        if not isinstance(self._opt, bool):
            raise(ASN1ObjErr('{0}: invalid opt, {1!r}'\
                  .format(self.fullname(), self._opt)))
        # default
        if not isinstance(self._def, NoneType):
            if self._mode == MODE_SET:
                self._safechk_set(self._def)
            else:
                self._safechk_val(self._def)
        # uniq
        if not isinstance(self._uniq, bool):
            raise(ASN1ObjErr('{0}: invalid uniq, {1!r}'\
                  .format(self.fullname(), self._uniq)))
        # group
        if not isinstance(self._group, (NoneType, integer_types)):
            raise(ASN1ObjErr('{0}: invalid group, {1!r}'\
                  .format(self.fullname(), self._group)))
    
    def _safechk_val(self, val):
        """
        ensures the value val has the correct format according to self
        """
        # check val format, implemented for each specific object
        pass
    
    def _safechk_val_int(self, val):
        if not isinstance(val, integer_types):
            raise(ASN1ObjErr('{0}: invalid INTEGER value, {1!r}'\
                  .format(self.fullname(), val)))
    
    def _safechk_val_real(self, val):
        if not isinstance(val, tuple) or len(val) != 3 or \
        not isinstance(val[0], integer_types) or \
        val[1] not in (2, 10) or \
        not isinstance(val[2], integer_types):
            raise(ASN1ObjErr('{0}: invalid REAL value, {1!r}'\
                  .format(self.fullname(), val)))
    
    def _safechk_val_str(self, val):
        if not isinstance(val, str_types):
            raise(ASN1ObjErr('{0}: invalid _String value, {1!r}'\
                  .format(self.fullname(), val)))
    
    def _safechk_set(self, s):
        """
        ensures the set of values s has the correct format according to self
        """
        if not isinstance(s, ASN1Set):
            raise(ASN1ObjErr('{0}: invalid set, {1!r}'.format(self.fullname(), s)))
        for v in s._rv:
            self._safechk_val(v)
        if s._ev is not None:
            for v in s._ev:
                self._safechk_val(v)
    
    def _safechk_set_int(self, si):
        if not isinstance(s, ASN1Set):
            raise(ASN1ObjErr('{0}: invalid set, {1!r}'.format(self.fullname(), s)))
        for v in s._rv:
            self._safechk_val(v)
        for vr in s._rr:
            if not isinstance(vr, ASN1RangeInt):
                raise(ASN1ObjErr('{0}: invalid INTEGER range, {1!r}'\
                      .format(self.fullname(), vr)))
        if s._ev is not None:
            for v in s._ev:
                self._safechk_val(v)
            for vr in s._er:
                if not isinstance(vr, ASN1RangeInt):
                    raise(ASN1ObjErr('{0}: invalid INTEGER range, {1!r}'\
                          .format(self.fullname(), vr)))
    
    def _safechk_set_real(self, sr):
        if not isinstance(s, ASN1Set):
            raise(ASN1ObjErr('{0}: invalid set, {1!r}'.format(self.fullname(), s)))
        for v in s._rv:
            self._safechk_val(v)
        for vr in s._rr:
            if not isinstance(vr, ASN1RangeReal):
                raise(ASN1ObjErr('{0}: invalid REAL range, {1!r}'\
                      .format(self.fullname(), vr)))
        if s._ev is not None:
            for v in s._evt:
                self._safechk_val(v)
            for vr in s._er:
                if not isinstance(vr, ASN1RangeReal):
                    raise(ASN1ObjErr('{0}: invalid REAL range, {1!r}'\
                          .format(self.fullname(), vr)))
    
    def _safechk_set_str(self, ss):
        if not isinstance(s, ASN1Set):
            raise(ASN1ObjErr('{0}: invalid set, {1!r}'.format(self.fullname(), s)))
        for v in s._rv:
            self._safechk_val(v)
        for vr in s._rr:
            if not isinstance(vr, ASN1RangeStr):
                raise(ASN1ObjErr('{0}: invalid _String range, {1!r}'\
                      .format(self.fullname(), vr)))
        if s._ev is not None:
            for v in s._ev:
                self._safechk_val(v)
            for vr in s._er:
                if not isinstance(vr, ASN1RangeStr):
                    raise(ASN1ObjErr('{0}: invalid _String range, {1!r}'\
                          .format(self.fullname(), vr)))
    
    def _safechk_bnd(self, val):
        """
        ensures the value val is within potential constraints defined for self
        """
        # check val against potential constraints
        if self._const_val and \
        self._const_val.ext is None and \
        val not in self._const_val:
            raise(ASN1ObjErr('{0}: value out of constraint, {1!r}'\
                  .format(self.fullname(), val)))
        if self._SAFE_BNDTAB and self._const_tab and self._const_tab_at:
            # check val against a constraint defined within the table constraint
            try:
                const_val = self._get_tab_obj()
            except Exception as err:
                if not self._SILENT:
                    asnlog('%s._from_per_ws: %s, unable to retrieve a defined object, %s'\
                           % (self.__class__.__name__, self._name, err))
            else:
                if self._mode == MODE_VALUE and val != const_val or \
                self._mode == MODE_SET and val not in const_val:
                    raise(ASN1ObjErr('{0}: value out of table constraint, {1!r}'\
                          .format(self.fullname(), val)))
    
    def _get_tab_obj(self):
        try:
            IndIdent = self._get_obj_by_path(self._const_tab_at)._const_tab_id
            IndVal = self._get_val_by_path(self._const_tab_at)
        except:
            raise(ASN1ObjErr('{0}: invalid table constraint @ path, {1!r}'\
                  .format(self.fullname(), self._const_tab_at)))
        try:
            claval = self._const_tab(IndIdent, IndVal)
        except:
            raise(ASN1ObjErr('{0}: invalid identifier {1} for the table constraint'\
                  .format(self.fullname(), IndIdent)))
        if claval is None:
            raise(ASN1ObjErr('{0}: non-existent value {1} for identifier {2} in the table constraint'\
                  .format(self.fullname(), IndVal, IndIdent)))
        else:
            try:
                return claval[self._const_tab_id]
            except:
                raise(ASN1ObjErr('{0}: non-existent ident {1} within table constraint value'\
                      .format(self.fullname(), self._const_tab_id)))
    
    #--------------------------------------------------------------------------#
    # user-friendly generic representation
    #--------------------------------------------------------------------------#
    
    def fullname(self):
        name = [self._name]
        obj = self
        while obj._parent is not None:
            obj = obj._parent
            name.append(obj._name)
        return '.'.join(reversed(name))
    
    def __repr__(self):
        if self._typeref is not None:
            if isinstance(self._typeref, (ASN1RefType, ASN1RefInstOf)):
                try:
                    typeref = self._typeref.called[1]
                except AttributeError:
                    # .called is ASN1RefParam
                    typeref = repr(self._typeref.called)
            elif isinstance(self._typeref, ASN1RefChoiceComp):
                try:
                    typeref = '%s<' % self._typeref.called[1] + \
                              '<'.join(self._typeref.ced_path)
                except AttributeError:
                    # .called is ASN1RefParam
                    typeref = '%s<' % repr(self._typeref.called) + \
                              '<'.join(self._typeref.ced_path)
            elif isinstance(self._typeref, ASN1RefClassField):
                try:
                    typeref = '%s.&' % self._typeref.called[1] + \
                              '.&'.join(self._typeref.ced_path)
                except AttributeError:
                    # .called is ASN1RefParam
                    typeref = '%s.&' % repr(self._typeref.called) + \
                              '.&'.join(self._typeref.ced_path)
            elif isinstance(self._typeref, ASN1RefClassIntern):
                typeref = '&%s' % '.&'.join(self._typeref.ced_path)
            elif isinstance(self._typeref, ASN1RefClassValField):
                try:
                    typeref = '%s.&%s' % (self._typeref.called[1],
                                          '.&'.join(self._typeref.ced_path))
                except AttributeError:
                    # .called is ASN1RefParam
                    typeref = '%s.&%s' % (repr(self._typeref.called),
                                          '.&'.join(self._typeref.ced_path))
            else:
                assert()
        else:
            typeref = None
        #
        if self._mode == MODE_TYPE:
            if typeref is not None:
                return '<%s ([%s] %s)>' % (self._name,
                                           typeref,
                                           self.TYPE)
            else:
                return '<%s (%s)>' % (self._name,
                                      self.TYPE)
        elif self._mode == MODE_VALUE:
            if self._val is not None:
                val = self._val
            else:
                val = ' '
            if typeref is not None:
                return '<%s ([%s] %s): %s>' % (self._name,
                                               typeref,
                                               self.TYPE,
                                               val)
            else:
                return '<%s (%s): %s>' % (self._name,
                                          self.TYPE,
                                          val)
        elif self._mode == MODE_SET:
            if self._val is not None:
                val = repr(self._val)
            else:
                val = ' '
            if typeref is not None:
                return '<%s ([%s] %s): %s>' % (self._name,
                                               typeref,
                                               self.TYPE,
                                               val)
            else:
                return '<%s (%s): %s>' % (self._name,
                                          self.TYPE,
                                          val)
    
    # TODO: make repr() and show() similar to those from pycrate_core.elt.Envelope
    
    #--------------------------------------------------------------------------#
    # internal attributes access method
    #--------------------------------------------------------------------------#
    
    def get_internals(self):
        """
        returns all internal attributes within a dict
        """
        internals = {
            'name'   : self._name,
            'mode'   : self._mode,
            'tag'    : self._tag,
            'typeref': self._typeref,
            'tr'     : self._tr,
            'param'  : self._param,
            'parent' : self._parent,
            'opt'    : self._opt,
            'def'    : self._def,
            'uniq'   : self._uniq,
            'group'  : self._group,
            'cont'   : self._cont,
            'root'   : self._root,
            'ext'    : self._ext,
            'val'    : self._val,
            'const_val': self._const_val,
            'const_tab': self._const_tab
            }
        if hasattr(self, 'cont_tags'):
            internals['cont_tags']      = self._cont_tags
        if hasattr(self, '_root_mand'):
            internals['root_mand']      = self._root_mand
        if hasattr(self, '_root_ext'):
            internals['root_ext']       = self._root_ext
        if hasattr(self, '_ext_group'):
            internals['ext_group']      = self._ext_group
        if hasattr(self, '_ext_group_obj'):
            internals['ext_group_obj']  = self._ext_group_obj
        if hasattr(self, '_ext_ident'):
            internals['ext_ident']      = self._ext_ident
        if hasattr(self, '_const_sz'):
            internals['const_sz']       = self._const_sz
        if hasattr(self, '_const_alpha'):
            internals['const_alpha']    = self._const_alpha
        if hasattr(self, '_const_cont'):
            internals['const_cont']     = self._const_cont
        if hasattr(self, '_const_cont_enc'):
            internals['const_cont_enc'] = self._const_cont_enc
        if hasattr(self, '_const_tab_id'):
            internals['const_tab_id']   = self._const_tab_id
        if hasattr(self, '_const_tab_at'):
            internals['const_tab_at']   = self._const_tab_at
        return internals
    
    def get_proto(self):
        """
        returns the prototype of the object
        """
        # TODO: in case of recursive object, this will break Python
        # TODO: add information on OPTIONAL / DEFAULT components
        if self.TYPE in TYPES_BASIC + TYPES_EXT:
            return self.TYPE
        elif self.TYPE in (TYPE_SEQ_OF, TYPE_SET_OF):
            return [self._cont.get_proto()]
        elif self.TYPE in (TYPE_CHOICE, TYPE_SEQ, TYPE_SET, TYPE_CLASS):
            return ASN1Dict([(ident, Comp.get_proto()) for (ident, Comp) in self._cont.items()])
        else:
            assert()
    
    def get_complexity(self):
        """
        returns the number of basic types referenced from self,
        and the maximum depth possible within self
        """
        ref, depth = 0, 0
        if self.TYPE in TYPES_BASIC + TYPES_EXT:
            ref += 1
        elif self.TYPE in (TYPE_SEQ_OF, TYPE_SET_OF):
            comp_ref, comp_depth = self._cont.get_complexity()
            ref += comp_ref
            depth += 1 + comp_depth
        elif self.TYPE in (TYPE_CHOICE, TYPE_SEQ, TYPE_SET, TYPE_CLASS):
            loc_depth = []
            for (ident, Comp) in self._cont.items():
                comp_ref, comp_depth = Comp.get_complexity()
                ref += comp_ref
                loc_depth.append(comp_depth)
            if loc_depth:
                depth += 1 + max(loc_depth)
        else:
            assert()
        return ref, depth
    
    def _get_obj_by_path(self, path):
        # this is used for solving table constraint lookups
        obj = self
        for p in path:
            if p == '..':
                obj = obj._parent
            else:
                obj = obj._cont[p]
        return obj
    
    def _get_val_by_path(self, path):
        # this is used for solving table constraint lookups
        obj = self
        for p in path:
            if p == '..':
                obj = obj._parent
            else:
                obj = obj._cont[p]
        return obj._val
    
    def get_root(self):
        """
        returns the root object containing self in case self is within a 
        constructed or CLASS object
        """
        # WNG: in case an object from a CLASS has been inserted after a table
        # constraint lookup, the parent will be incorrect 
        par = self._parent
        while par is not None:
            par = self._parent
        return par
    
    def get_root_chain(self):
        """
        returns the list of successive objects containing self up to the root one
        in case self is within a constructed or CLASS object
        """
        # WNG: in case an object from a CLASS has been inserted after a table
        # constraint lookup, the parent will be incorrect 
        par   = self._parent
        chain = []
        while par is not None:
            chain.append(par)
            par = par._parent
        return chain
    
    def in_class(self):
        """
        returns True in case self is a field of a CLASS, False otherwise
        """
        par = self._parent
        while par is not None:
            if par.TYPE == TYPE_CLASS:
                return True
            else:
                par = par._parent
        return False
    
    #--------------------------------------------------------------------------#
    # internal value access methods
    #--------------------------------------------------------------------------#
    
    def __call__(self):
        return self._val
    
    def get_val(self):
        return self._val
    
    def set_val(self, val):
        self._val = val
        if self._SAFE_VAL:
            self._safechk_val(self._val)
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    def set_val_unsafe(self, val):
        self._val = val
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    #--------------------------------------------------------------------------#
    # encoding / decoding methods
    #--------------------------------------------------------------------------#
    
    ###
    # conversion between internal value and ASN.1 syntax
    ###
    
    def _from_asn1(self, txt):
        raise(ASN1NotSuppErr(self.fullname()))
    
    def _to_asn1(self):
        raise(ASN1NotSuppErr(self.fullname()))
    
    def from_asn1(self, txt):
        txt = clean_text(txt)
        ret = self._from_asn1(txt)
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
        return ret
    
    def to_asn1(self, val=None):
        if val is not None:
            self.set_val(val)
        if self._val is not None:
            return self._to_asn1()
        else:
            return None
    
    def show(self):
        return '<~ASN1~: %s>' % self.to_asn1()
    
    ###
    # conversion between internal value and ASN.1 PER encoding
    ###
    
    def _from_per(self, char):
        raise(ASN1NotSuppErr(self.fullname()))
    
    def _to_per(self):
        raise(ASN1NotSuppErr(self.fullname()))
    
    def from_uper(self, buf):
        ASN1CodecPER.ALIGNED = False
        if isinstance(buf, bytes_types):
            char = Charpy(buf)
        else:
            char = buf
            #assert( char.len_bit() % 8 == 0 )
        off0 = char._cur
        self._from_per(char)
        off1 = char._cur
        if off1 == off0:
            # char was not consumed at all (all decoded values were implicit)
            # hence a null byte must be consumed for outer decoding
            null = char.get_bytes(8)
            assert( null == b'\0' )
        elif (off1 - off0) % 8:
            # realignement required for outer decoding
            char.forward(8 - ((off1 - off0)%8))
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    def to_uper(self, val=None):
        ASN1CodecPER.ALIGNED = False
        if val is not None:
            self.set_val(val)
        if self._val is not None:
            ret = pack_val(*self._to_per())[0]
            if ret:
                return ret
            else:
                return b'\0'
        else:
            return None
    
    def from_aper(self, buf):
        ASN1CodecPER.ALIGNED = True
        ASN1CodecPER._off.append(0)
        if isinstance(buf, bytes_types):
            char = Charpy(buf)
        else:
            char = buf
            assert( char.len_bit() % 8 == 0 )
        self._from_per(char)
        if ASN1CodecPER._off[-1] == 0:
            # char was not consumed at all (all decoded values were implicit)
            # hence a null byte must be consumed
            null = char.get_bytes(8)
            assert( null == b'\0' )
        elif ASN1CodecPER._off[-1] % 8:
            # realignement required for outer decoding
            char.forward(8 - (ASN1CodecPER._off[-1]%8))
        del ASN1CodecPER._off[-1]
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    def to_aper(self, val=None):
        ASN1CodecPER.ALIGNED = True
        if val is not None:
            self.set_val(val)
        if self._val is not None:
            ASN1CodecPER._off.append(0)
            ret = pack_val(*self._to_per())[0]
            if not ret:
                ret = b'\0'
            del ASN1CodecPER._off[-1]
            return ret
        else:
            return None
     
    # methods generating complete transfer structure in _struct attributes
    
    def _from_per_ws(self, char):
        raise(ASN1NotSuppErr(self.fullname()))
    
    def _to_per_ws(self):
        raise(ASN1NotSuppErr(self.fullname()))
    
    def from_uper_ws(self, buf):
        ASN1CodecPER.ALIGNED = False
        if isinstance(buf, bytes_types):
            char = Charpy(buf)
        else:
            char = buf
            #assert( char.len_bit() % 8 == 0 )
        off0 = char._cur
        self._from_per_ws(char)
        off1 = char._cur
        pad = None
        if off1 == off0:
            # char was not consumed at all (all decoded values were implicit)
            # hence a null byte must be consumed
            pad = Uint('P', val=0, bl=8, rep=REPR_BIN)
            pad._from_char(char)
            self._struct.append(pad)
            assert( pad() == 0 )
        elif (off1 - off0) % 8:
            # realignment required for outer decoding
            pad = Uint('P', val=0, bl=(8-((off1 - off0)%8)), rep=REPR_BIN)
            pad._from_char(char)
            self._struct.append(pad)
            assert( pad() == 0 )
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    def to_uper_ws(self, val=None):
        ASN1CodecPER.ALIGNED = False
        if val is not None:
            self.set_val(val)
        if self._val is not None:
            _struct = self._to_per_ws()
            bl = _struct.get_bl()
            if bl == 0:
                _struct.append( Uint('P', val=0, bl=8, rep=REPR_BIN) )
            elif bl % 8:
                _struct.append( Uint('P', val=0, bl=(8-(bl%8)), rep=REPR_BIN) )
            return _struct.to_bytes()
        else:
            return None
    
    def from_aper_ws(self, buf):
        ASN1CodecPER.ALIGNED = True
        ASN1CodecPER._off.append(0)
        if isinstance(buf, bytes_types):
            char = Charpy(buf)
        else:
            char = buf
            assert( char.len_bit() % 8 == 0 )
        self._from_per_ws(char)
        if ASN1CodecPER._off[-1] == 0:
            # char was not consumed at all (all decoded values were implicit)
            # hence a null byte must be consumed
            pad = Uint('P', val=0, bl=8, rep=REPR_BIN)
            pad._from_char(char)
            self._struct.append(pad)
            assert( pad() == 0 )
        elif ASN1CodecPER._off[-1] % 8:
            # realignement required for outer decoding
            pad = Uint('P', val=0, bl=(8 - (ASN1CodecPER._off[-1]%8)), rep=REPR_BIN)
            pad._from_char(char)
            self._struct.append(pad)
            assert( pad() == 0 )
        del ASN1CodecPER._off[-1]
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    def to_aper_ws(self, val=None):
        ASN1CodecPER.ALIGNED = True
        if val is not None:
            self.set_val(val)
        if self._val is not None:
            ASN1CodecPER._off.append(0)
            _struct = self._to_per_ws()
            if ASN1CodecPER._off[-1] == 0:
                _struct.append( Uint('P', val=0, bl=8, rep=REPR_BIN) )
            elif ASN1CodecPER._off[-1] % 8:
                _struct.append( Uint('P', val=0, bl=(8-(ASN1CodecPER._off[-1]%8)), rep=REPR_BIN) )
            del ASN1CodecPER._off[-1]
            return _struct.to_bytes()
        else:
            return None
    
    ###
    # conversion between internal value and ASN.1 BER encoding
    ###
    
    def _from_ber(self, char, TLV):
        # 1) decode the tag chain
        tlv, pc = TLV, 1
        for t in self._tagc:
            tlv = tlv[0]
            cl, pc, tval, lval = tlv[0:4]
            if (cl, tval) != t or (t != self._tagc[-1] and pc == 0):
                raise(ASN1BERDecodeErr('{0}: invalid tag class / pc / value, {1!r}'\
                      .format(self.fullname(), (cl, pc, tval))))
            tlv = tlv[4]
        if pc == 0:
            # 2a) decode primitive content value
            # here, tlv is actually a 2-tuple with the value boundaries
            assert( isinstance(tlv, tuple) )
        else:
            # 2b) decode constructed content
            assert( isinstance(tlv, list) )
        self._decode_ber_cont(char, tlv)
    
    def from_ber(self, buf, single=True):
        if isinstance(buf, bytes_types):
            char = Charpy(buf)
        else:
            char = buf
        # decode the whole char buffer into tag, length and value boundary
        if single:
            TLV = [ASN1CodecBER.decode_single(char)[0]]
        else:
            TLV = ASN1CodecBER.decode_all(char)
        char_cur, char_lb = char._cur, char._len_bit
        # decode all value content
        self._from_ber(char, TLV)
        char._cur, char._len_bit = char_cur, char_lb
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    def _to_ber(self):
        # 1) encode the most inner TLV part
        pc, lval, V = self._encode_ber_cont()
        if not self._tagc:
            # in case no tag is associated to the object (CHOICE, OPEN / ANY)
            # we only have the inner encoding
            return V
        TLV = ASN1CodecBER.encode_tag(self._tagc[-1][0], pc, self._tagc[-1][1])
        TLV.extend( ASN1CodecBER.encode_len(lval) )
        TLV.extend( V )
        if lval == -1:
            TLV.append( (T_BYTES, b'\0\0', 16) )
        # 2) encode the outer part of the object, i.e. the rest of the tag chain
        if len(self._tagc) > 1:
            GEN = [TLV]
            if ASN1CodecBER.ENC_LUNDEF:
                for t in reversed(self._tagc[:-1]):
                    TL = ASN1CodecBER.encode_tag(t[0], 1, t[1])
                    TL.extend( ASN1CodecBER.encode_len(-1) )
                    # append an EOC marker after the value
                    TLV.append( (T_BYTES, b'\0\0', 16) )
                    GEN.append(TL)
            else:
                lval = sum([f[2] for f in TLV]) >> 3
                for t in reversed(self._tagc[:-1]):
                    TL = ASN1CodecBER.encode_tag(t[0], 1, t[1])
                    TL.extend( ASN1CodecBER.encode_len(lval) )
                    lval += sum([f[2] for f in TL]) >> 3
                    GEN.append(TL)
            # revert and flatten GEN
            return [i for j in reversed(GEN) for i in j]
        else:
            return TLV
    
    def to_ber(self, val=None):
        if val is not None:
            self.set_val(val)
        if self._val is not None:
            return pack_val(*self._to_ber())[0]
        else:
            return None
    
    # methods generating complete transfer structure in _struct attributes
    
    def _from_ber_ws(self, char, TLV):
        # 1) decode the tag chain
        tlv, TL, pc = TLV, [], 1
        for t in self._tagc:
            tlv = tlv[0]
            Tag, cl, pc, tval, Len, lval = tlv[0:6]
            if (cl, tval) != t or (t != self._tagc[-1] and pc == 0):
                raise(ASN1BERDecodeErr('{0}: invalid tag class / pc / value, {1!r}'\
                      .format(self.fullname(), (cl, pc, tval))))
            # select the value as a new inner tlv
            tlv = tlv[6]
            if lval == -1:
                # keep track of the End-Of-Content marker
                TL.append( (Tag, Len, tlv[-1][0], tlv[-1][4]) )
            else:
                TL.append( (Tag, Len) )
        #
        if pc == 0:
            # 2a) decode primitive content value
            # here, tlv is actually a 2-tuple with the value boundaries
            assert( isinstance(tlv, tuple) )
        else:
            # 2b) decode constructed content
            assert( isinstance(tlv, list) )
        V = self._decode_ber_cont_ws(char, tlv)
        #
        # 3) generate the complete TLV structure
        if TL:
            TL.reverse()
            for tl in TL:
                if len(tl) == 4:
                    # EOC marker
                    TLV = Envelope('TLV', GEN=(tl[0], tl[1], V, tl[2], tl[3]))
                else:
                    TLV = Envelope('TLV', GEN=(tl[0], tl[1], V))
                V = TLV
                V._name = 'V'
        else:
            # in case no tag is associated to the object (CHOICE, OPEN / ANY)
            # we only have the inner encoding
            TLV = V
        # set object name and final struct
        TLV._name = self._name
        self._struct = TLV
    
    def from_ber_ws(self, buf, single=True):
        if isinstance(buf, bytes_types):
            char = Charpy(buf)
        else:
            char = buf
        # decode the whole char buffer into tag, length and value boundary
        if single:
            TLV = [ASN1CodecBER.decode_single_ws(char)[0]]
        else:
            TLV = ASN1CodecBER.decode_all_ws(char)
        char_cur, char_lb = char._cur, char._len_bit
        # decode all value content
        self._from_ber_ws(char, TLV)
        char._cur, char._len_bit = char_cur, char_lb
        if self._SAFE_BND:
            self._safechk_bnd(self._val)
    
    def _to_ber_ws(self):
        # 1) encode the most inner TLV part
        pc, lval, V = self._encode_ber_cont_ws()
        if not self._tagc:
            # in case no tag is associated to the object (CHOICE, OPEN / ANY)
            # we only have the inner encoding
            TLV = V
        else:
            if pc == 1 and ASN1CodecBER.ENC_LUNDEF:
                TLV = Envelope('TLV', GEN=(
                        ASN1CodecBER.encode_tag_ws(self._tagc[-1][0], pc, self._tagc[-1][1]),
                        ASN1CodecBER.encode_len_ws(-1),
                        V,
                        ASN1CodecBER.encode_tag_ws(0, 0, 0),
                        ASN1CodecBER.encode_len_ws(0)))
            else:
                TLV = Envelope('TLV', GEN=(
                        ASN1CodecBER.encode_tag_ws(self._tagc[-1][0], pc, self._tagc[-1][1]),
                        ASN1CodecBER.encode_len_ws(lval),
                        V))
            # 2) encode the outer part of the object, i.e. the rest of the tag chain
            if len(self._tagc) > 1:
                if ASN1CodecBER.ENC_LUNDEF:
                    for t in reversed(self._tagc[:-1]):
                        TLV._name = 'V'
                        TLV = Envelope('TLV', GEN=(ASN1CodecBER.encode_tag_ws(t[0], 1, t[1]),
                                                      ASN1CodecBER.encode_len_ws(-1),
                                                      TLV,
                                                      ASN1CodecBER.encode_tag_ws(0, 0, 0),
                                                      ASN1CodecBER.encode_len_ws(0)))
                else:
                    lval += (TLV[0].get_bl() + TLV[1].get_bl()) >> 3
                    for t in reversed(self._tagc[:-1]):
                        TLV._name = 'V'
                        TLV = Envelope('TLV', GEN=(ASN1CodecBER.encode_tag_ws(t[0], 1, t[1]),
                                                      ASN1CodecBER.encode_len_ws(lval),
                                                      TLV))
                        lval += (TLV[0].get_bl() + TLV[1].get_bl()) >> 3
        # set object name and final struct
        TLV._name = self._name
        self._struct = TLV
        return TLV
    
    def to_ber_ws(self, val=None):
        if val is not None:
            self.set_val(val)
        if self._val is not None:
            return self._to_ber_ws().to_bytes()
        else:
            return None
    
    ###
    # conversion between internal value and ASN.1 CER encoding
    # reusing the BER encoder
    ###
    
    def from_cer(self, buf):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = True
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 1000
        ASN1CodecBER.ENC_OSTR_FRAG  = 1000
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.from_ber(buf)
        _restore_ber_params()
        return ret
    
    def to_cer(self, val=None):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = True
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 1000
        ASN1CodecBER.ENC_OSTR_FRAG  = 1000
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.to_ber(val)
        _restore_ber_params()
        return ret
    
    # methods generating complete transfer structure in _struct attributes
    
    def from_cer_ws(self, buf):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = True
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 1000
        ASN1CodecBER.ENC_OSTR_FRAG  = 1000
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.from_ber_ws(buf)
        _restore_ber_params()
        return ret
    
    def to_cer_ws(self, val=None):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = True
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 1000
        ASN1CodecBER.ENC_OSTR_FRAG  = 1000
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.to_ber_ws(val)
        _restore_ber_params()
        return ret
    
    ###
    # conversion between internal value and ASN.1 DER encoding
    # reusing the BER encoder
    ###
    
    def from_der(self, buf):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = False
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 0
        ASN1CodecBER.ENC_OSTR_FRAG  = 0
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.from_ber(buf)
        _restore_ber_params()
        return ret
    
    def to_der(self, val=None):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = False
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 0
        ASN1CodecBER.ENC_OSTR_FRAG  = 0
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.to_ber(val)
        _restore_ber_params()
        return ret
    
    # methods generating complete transfer structure in _struct attributes
    
    def from_der_ws(self, buf):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = False
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 0
        ASN1CodecBER.ENC_OSTR_FRAG  = 0
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.from_ber_ws(buf)
        _restore_ber_params()
        return ret
    
    def to_der_ws(self, val=None):
        _save_ber_params()
        ASN1CodecBER.ENC_LLONG      = 0
        ASN1CodecBER.ENC_LUNDEF     = False
        ASN1CodecBER.ENC_BOOLTRUE   = 0xff
        ASN1CodecBER.ENC_REALNR     = 3
        ASN1CodecBER.ENC_BSTR_FRAG  = 0
        ASN1CodecBER.ENC_OSTR_FRAG  = 0
        ASN1CodecBER.ENC_TIME_CANON = True
        ASN1CodecBER.ENC_DEF_CANON  = True
        ret = self.to_ber_ws(val)
        _restore_ber_params()
        return ret
    
    ###
    # convert internal value to ASN.1 GSER encoding
    ###
    # TODO
    
    def from_gser(self, char):
        raise(ASN1NotSuppErr(self.fullname()))
    
    def to_gser(self, buf, val=None):
        raise(ASN1NotSuppErr(self.fullname()))



def _save_ber_params():
    global __ber_enc_llong
    global __ber_enc_lundef
    global __ber_enc_booltrue
    global __ber_enc_realnr
    global __ber_enc_bstr_frag
    global __ber_enc_ostr_frag
    global __ber_enc_time_canon
    global __ber_enc_def_canon
    __ber_enc_llong      = ASN1CodecBER.ENC_LLONG
    __ber_enc_lundef     = ASN1CodecBER.ENC_LUNDEF
    __ber_enc_booltrue   = ASN1CodecBER.ENC_BOOLTRUE
    __ber_enc_realnr     = ASN1CodecBER.ENC_REALNR
    __ber_enc_bstr_frag  = ASN1CodecBER.ENC_BSTR_FRAG
    __ber_enc_ostr_frag  = ASN1CodecBER.ENC_OSTR_FRAG
    __ber_enc_time_canon = ASN1CodecBER.ENC_TIME_CANON
    __ber_enc_def_canon  = ASN1CodecBER.ENC_DEF_CANON

def _restore_ber_params():
    global __ber_enc_llong
    global __ber_enc_lundef
    global __ber_enc_booltrue
    global __ber_enc_realnr
    global __ber_enc_bstr_frag
    global __ber_enc_ostr_frag
    global __ber_enc_time_canon
    global __ber_enc_def_canon
    ASN1CodecBER.ENC_LLONG      = __ber_enc_llong
    ASN1CodecBER.ENC_LUNDEF     = __ber_enc_lundef
    ASN1CodecBER.ENC_BOOLTRUE   = __ber_enc_booltrue
    ASN1CodecBER.ENC_REALNR     = __ber_enc_realnr
    ASN1CodecBER.ENC_BSTR_FRAG  = __ber_enc_bstr_frag
    ASN1CodecBER.ENC_OSTR_FRAG  = __ber_enc_ostr_frag
    ASN1CodecBER.ENC_TIME_CANON = __ber_enc_time_canon
    ASN1CodecBER.ENC_DEF_CANON  = __ber_enc_def_canon

